package com.epam.ws.anatomist.message;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.dom4j.io.XMLResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import com.epam.ws.anatomist.common.WsAnatomistUtil;
import com.epam.ws.anatomist.common.exception.ErrorMessages;
import com.epam.ws.anatomist.common.exception.MessageConversionException;
import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

public class MessageConverter
{

	private static final Logger LOGGER = Logger.getLogger(MessageConverter.class);

	private static final StreamSource XML_SOURCE = new StreamSource();

	private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();

	private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();

	@SuppressWarnings("unchecked")
	public static synchronized <T> T convertXmlToPojo(Class<T> pojoType, String xml)
	{
		if (pojoType == null || xml == null)
		{
			return null;
		}
		StringReader xmlReader = null;
		try
		{
			JAXBContext context = JAXBContext.newInstance(pojoType);
			xmlReader = new StringReader(xml);
			XML_SOURCE.setReader(xmlReader);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			T result = (T) unmarshaller.unmarshal(XML_SOURCE);
			if (result instanceof JAXBElement<?>)
			{
				return ((JAXBElement<T>) result).getValue();
			}
			return result;
		}
		catch (Exception e)
		{
			LOGGER.error(ErrorMessages.CONVERSION_XML_TO_POJO_ERROR);
			throw new MessageConversionException(ErrorMessages.CONVERSION_XML_TO_POJO_ERROR, e);
		}
		finally
		{
			IOUtils.closeQuietly(xmlReader);
		}
	}

	public static String convertToXml(Object source)
	{
		String result;
		StringWriter sw = new StringWriter();
		try
		{
			JAXBContext carContext = JAXBContext.newInstance(source.getClass());
			Marshaller carMarshaller = carContext.createMarshaller();
			carMarshaller.marshal(source, sw);
			result = sw.toString();
		}
		catch (JAXBException e)
		{
			throw new RuntimeException(e);
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	public static synchronized <T> String convertPojoToXml(T pojo)
	{
		if (pojo == null)
		{
			return null;
		}
		Class<T> pojoClass = (Class<T>) pojo.getClass();
		StringWriter writer = new StringWriter();
		try
		{
			if (pojoClass.isAnnotationPresent(XmlRootElement.class))
			{

				JAXBContext context = JAXBContext.newInstance(pojo.getClass());
				Marshaller marshaller = context.createMarshaller();
				marshaller.marshal(pojo, writer);
			}
			else
			{
				String packagename = WsAnatomistUtil.getPackageName(pojoClass);
				String uri = "";
				XmlType xmlTypeAnnotation = pojoClass.getAnnotation(XmlType.class);
				if (xmlTypeAnnotation != null)
				{
					uri = xmlTypeAnnotation.namespace();
				}
				else
				{
					uri = "urn:" + packagename.replace('.', ':');
				}
				String className = pojoClass.getSimpleName();
				JAXBContext jaxbContext = JAXBContext.newInstance(packagename);
				Marshaller marshaller = jaxbContext.createMarshaller();
				JAXBElement<T> jaxbElement = new JAXBElement<T>(new QName(uri, className), pojoClass, pojo);
				marshaller.marshal(jaxbElement, writer);
			}
			return writer.toString();
		}
		catch (Exception e)
		{
			LOGGER.error(ErrorMessages.CONVERSION_POJO_TO_XML_ERROR);
			throw new MessageConversionException(ErrorMessages.CONVERSION_POJO_TO_XML_ERROR, e);
		}
		finally
		{
			IOUtils.closeQuietly(writer);
		}
	}

	public static synchronized <T> String convertPojoToXmlAndFormat(T pojo)
	{
		String xml = convertPojoToXml(pojo);
		Document document = convertXmlToDocument(xml);
		return xmlToString(document.getDocumentElement());

	}

	public static <T> Document convertPojoToDocument(T pojo)
	{
		String xml = MessageConverter.convertToXml(pojo);
		return MessageConverter.convertXmlToDocument(xml);
	}

	public static Document convertXmlToDocument(String xml)
	{
		if (xml == null)
		{
			return null;
		}
		StringReader stringReader = new StringReader(xml);
		InputSource inputSource = new InputSource(stringReader);
		try
		{
			return createDocumentBuilder().parse(inputSource);
		}
		catch (Exception e)
		{
			LOGGER.error(ErrorMessages.CONVERSION_XML_TO_DOCUMENT_ERROR);
			throw new MessageConversionException(ErrorMessages.CONVERSION_XML_TO_DOCUMENT_ERROR, e);
		}
		finally
		{
			IOUtils.closeQuietly(stringReader);
		}
	}

	public static String convertDocumentToXml(Document doc)
	{
		DOMSource domSource = new DOMSource(doc);
		StringWriter writer = new StringWriter();
		StreamResult result = new StreamResult(writer);
		Transformer transformer = createTransformer();
		try
		{
			transformer.transform(domSource, result);
			return writer.toString();
		}
		catch (TransformerException e)
		{
			LOGGER.error(ErrorMessages.DOCUMENT_TO_XML_TRANSFORMING_ERROR);
			throw new MessageConversionException(ErrorMessages.DOCUMENT_TO_XML_TRANSFORMING_ERROR, e);
		}
		finally
		{
			IOUtils.closeQuietly(writer);
		}
	}

	public static <T> T convertDocumentToPojo(Class<T> pojoClass, Document document)
	{
		String xml = convertDocumentToXml(document);
		return convertXmlToPojo(pojoClass, xml);
	}

	public static String convertSourceToXml(Source source)
	{
		final Transformer transformer = createTransformer();

		return transformSourceToXml(source, transformer);
	}

	public static String xmlToString(Element elem)
	{
		StringWriter writer = new StringWriter();
		try
		{
			OutputFormat format = new OutputFormat();
			format.setLineWidth(0);
			format.setIndenting(false);
			format.setIndent(4);
			format.setOmitXMLDeclaration(false);
			format.setOmitDocumentType(false);
			XMLSerializer serializer = new XMLSerializer(writer, format);
			serializer.setNamespaces(true);
			try
			{
				serializer.serialize(elem);
			}
			catch (IOException e)
			{
				LOGGER.error(ErrorMessages.CONVERSION_XML_TO_STRING_ERROR);
				throw new MessageConversionException(ErrorMessages.CONVERSION_XML_TO_STRING_ERROR, e);
			}
			return writer.toString();
		}
		finally
		{
			IOUtils.closeQuietly(writer);
		}
	}

	private static String transformSourceToXml(Source source, Transformer transformer)
	{
		try
		{
			XMLResult xmlResult = new XMLResult();
			transformer.transform(source, xmlResult);
			return xmlResult.toString();
		}
		catch (Exception e)
		{
			LOGGER.error(ErrorMessages.CONVERSION_SOURCE_TO_XML_ERROR);
			throw new MessageConversionException(ErrorMessages.CONVERSION_SOURCE_TO_XML_ERROR, e);
		}
	}

	private static DocumentBuilder createDocumentBuilder()
	{
		try
		{
			return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
		}
		catch (ParserConfigurationException e)
		{
			LOGGER.error(ErrorMessages.DOCUMENT_BUILDER_CREATION_ERROR);
			throw new MessageConversionException(ErrorMessages.DOCUMENT_BUILDER_CREATION_ERROR, e);
		}
	}

	private static Transformer createTransformer()
	{
		try
		{
			return TRANSFORMER_FACTORY.newTransformer();
		}
		catch (TransformerConfigurationException e)
		{
			LOGGER.error(ErrorMessages.TRANSAFORMER_ERROR);
			throw new MessageConversionException(ErrorMessages.TRANSAFORMER_ERROR, e);
		}
	}

}
